{
  Copyright 2012 Sergey Ostanin

  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
}

unit SectionTree;

interface

uses
  Classes, SysUtils, FileUtil, Forms, Controls, StdCtrls, ComCtrls, ActnList,
  Menus, superobject, Scene, VisualUtils, TestCore, MiscUtils, InputString,
  Shape, LCLType, Clipbrd, TestUiUtils, Graphics, ModifierListEditor,
  ItImport, base64, ItExport, Dialogs;

type
  TSectionTreeFrame = class(TShapeFrame)
    actAdd: TAction;
    actDelete: TAction;
    actEditModifiers: TAction;
    actMoveDown: TAction;
    actMoveUp: TAction;
    actPaste: TAction;
    actSetName: TAction;
    alSections: TActionList;
    ilSections: TImageList;
    lblSections: TLabel;
    miEditModifiers: TMenuItem;
    miSeparator4: TMenuItem;
    miSeparator3: TMenuItem;
    miMoveDown: TMenuItem;
    miMoveUp: TMenuItem;
    miPaste: TMenuItem;
    miSeparator2: TMenuItem;
    miSetName: TMenuItem;
    miDelete: TMenuItem;
    miSeparator1: TMenuItem;
    miAdd: TMenuItem;
    pmSections: TPopupMenu;
    tbrSections: TToolBar;
    tbAdd: TToolButton;
    tbDelete: TToolButton;
    tbSeparator1: TToolButton;
    tbMoveUp: TToolButton;
    tbMoveDown: TToolButton;
    tbSeparator2: TToolButton;
    tbEditModifiers: TToolButton;
    tvSections: TTreeView;
    procedure actAddExecute(Sender: TObject);
    procedure actDeleteExecute(Sender: TObject);
    procedure actDeleteUpdate(Sender: TObject);
    procedure actEditModifiersExecute(Sender: TObject);
    procedure actMoveDownExecute(Sender: TObject);
    procedure actMoveUpExecute(Sender: TObject);
    procedure actPasteExecute(Sender: TObject);
    procedure actPasteUpdate(Sender: TObject);
    procedure actSetNameExecute(Sender: TObject);
    procedure tvSectionsCustomDrawItem(Sender: TCustomTreeView;
      Node: TTreeNode; State: TCustomDrawState; var DefaultDraw: Boolean);
    procedure tvSectionsSelectionChanged(Sender: TObject);
  private
    FSelection: ISuperObject;
    FRendered: Boolean;
    FSectionImageIndex: Integer;
    FSectionWithModifiersImageIndex: Integer;
    procedure RenderSections(ChangedOnly: Boolean);
    procedure RenderSection(Node: TTreeNode; OnlyIfChanged: Boolean);
    procedure CreateSectionNode(const Node: ISuperObject; Parent: TTreeNode);
    procedure RenderSelection;
    function GetNode(const EditorId: TShapeId): TTreeNode;
    function SelectedEditorId: TShapeId;
    function SelectedEditor: ISuperObject;
    function GetNodeEditorId(Node: TTreeNode): TShapeId;
    function GetNodeEditor(Node: TTreeNode): ISuperObject;
    procedure UpdateActions;
    procedure MoveSection(Down: Boolean);
    procedure GetModifiers(List: TModifierList);
    procedure SetModifiers(List: TModifierList);
    { private declarations }
  public
    procedure AfterConstruction; override;
    destructor Destroy; override;
    procedure Render;
    { public declarations }
  end; 

implementation

{$R *.lfm}
{$R *.rc}

resourcestring
  SRenameSection = 'Rename Section';
  SNewSectionName = 'New name:';
  SAddSection = 'Add Section';
  SAddSectionButtonText = 'Add';
  SSectionName = 'Section name:';
  SDeleteSection = 'Delete section "%s"?';
  SConfirmation = 'Confirmation';
  SDelete = 'Delete';

type
  TSectionHolder = class
  private
    FEditorId: TShapeId;
    FEditor: ISuperObject;
  public
    constructor Create(const EditorId: TShapeId);
  end;

{ TSectionHolder }

constructor TSectionHolder.Create(const EditorId: TShapeId);
begin
  inherited Create;
  FEditorId := EditorId;
end;

{ TSectionTreeFrame }

procedure TSectionTreeFrame.AfterConstruction;
begin
  inherited;
  alSections.OnExecute := Scene.ActionListExecuteFilter;
  LoadActionImages(alSections);
  tvSections.Options := tvSections.Options - [tvoHideSelection]
    + [tvoReadOnly, tvoHotTrack, tvoRightClickSelect];

  FSectionImageIndex := AddPngResourceToImageList('SectionTree/section.png', ilSections);
  FSectionWithModifiersImageIndex := AddPngResourceToImageList('SectionTree/sectionWithModifiers.png', ilSections);
end;

destructor TSectionTreeFrame.Destroy;
begin
  if tvSections <> nil then
  begin
    tvSections.OnSelectionChanged := nil;
    ClearTreeViewAndFreeDataObjects(tvSections);
  end;
  inherited;
end;

procedure TSectionTreeFrame.Render;
var
  ShapeChanged, SelectionChanged: Boolean;
  Saved: TNotifyEvent;
begin
  ShapeChanged := UpdateShape;
  SelectionChanged := Scene.Update(Shape.S['selection'], FSelection);

  Saved := tvSections.OnSelectionChanged;
  try
    tvSections.OnSelectionChanged := nil;
    tvSections.BeginUpdate;
    try
      if not FRendered or ShapeChanged then
      begin
        ClearTreeViewAndFreeDataObjects(tvSections);
        CreateSectionNode(Shape['root'], nil);
        RenderSections(FALSE);
        tvSections.Items.GetFirstNode.Expand(TRUE);
        RenderSelection;
      end
      else
      begin
        RenderSections(TRUE);
        if SelectionChanged then
          RenderSelection;
      end;

      { Without this, the horizontal scrollbar is not updated after renaming a section. }
      tvSections.ScrollBars := ssBoth;
      tvSections.ScrollBars := ssAutoBoth;
    finally
      tvSections.EndUpdate;
    end;
  finally
    tvSections.OnSelectionChanged := Saved;
  end;

  UpdateActions;
  FRendered := TRUE;
end;

function TSectionTreeFrame.GetNode(const EditorId: TShapeId): TTreeNode;
begin
  Result := tvSections.Items.GetFirstNode;
  while Result <> nil do
  begin
    if GetNodeEditorId(Result) = EditorId then
      Break;
    Result := Result.GetNext;
  end;
  Assert( Result <> nil );
end;

procedure TSectionTreeFrame.RenderSection(Node: TTreeNode; OnlyIfChanged: Boolean);
var
  h: TSectionHolder;
  EditorChanged: Boolean;
  ImageIndex: Integer;
begin
  h := Node.Data;
  EditorChanged := Scene.Update(h.FEditorId, h.FEditor);
  if not OnlyIfChanged or EditorChanged then
  begin
    Node.Text := TSection.GetFriendlyName(h.FEditor.S['name']);

    if h.FEditor.B['hasModifiers'] then
      ImageIndex := FSectionWithModifiersImageIndex
    else
      ImageIndex := FSectionImageIndex;
    Node.ImageIndex := ImageIndex;
    Node.SelectedIndex := ImageIndex;
  end;
end;

procedure TSectionTreeFrame.tvSectionsSelectionChanged(Sender: TObject);
var
  Input: ISuperObject;
begin
  Input := SO;
  Input.S['id'] := SelectedEditorId;
  Dispatcher.PerformAction(Id, 'select', Input);
end;

procedure TSectionTreeFrame.actSetNameExecute(Sender: TObject);
var
  NewName: String;
  Input: ISuperObject;
begin
  NewName := SelectedEditor.S['name'];
  if TInputStringForm.OpenModal(SRenameSection, SNewSectionName, NewName,
    Shape.I['maxSectionNameLength'], nil) then
  begin
    Input := SO;
    Input.S['name'] := CollapseSpaces(NewName);
    Dispatcher.PerformAction(SelectedEditorId, 'setName', Input);
  end;
end;

procedure TSectionTreeFrame.tvSectionsCustomDrawItem(Sender: TCustomTreeView;
  Node: TTreeNode; State: TCustomDrawState; var DefaultDraw: Boolean);
var
  fs: TFontStyles;
begin
  if TSectionHolder(Node.Data).FEditor.I['questionCount'] > 0 then
    fs := [fsBold]
  else
    fs := [];
  tvSections.Canvas.Font.Style := fs;
end;

procedure TSectionTreeFrame.actAddExecute(Sender: TObject);
var
  Name: String;
  Input: ISuperObject;
begin
  Name := '';
  if TInputStringForm.OpenModal(SAddSection, SSectionName, Name,
    Shape.I['maxSectionNameLength'], nil, SAddSectionButtonText) then
  begin
    Input := SO;
    Input.S['name'] := CollapseSpaces(Name);
    Dispatcher.PerformAction(Id, 'add', Input);
  end;
end;

procedure TSectionTreeFrame.actDeleteExecute(Sender: TObject);
begin
  if QuestionDlg(SConfirmation,
    Format(SDeleteSection, [TSection.GetFriendlyName(SelectedEditor.S['name'])]),
    mtConfirmation, [mrYes, SDelete, mrCancel], 0) = mrYes then
    Dispatcher.PerformAction(Id, 'delete');
end;

procedure TSectionTreeFrame.actDeleteUpdate(Sender: TObject);
begin
  (Sender as TAction).Enabled := tvSections.Selected <> tvSections.Items.GetFirstNode;
end;

procedure TSectionTreeFrame.actEditModifiersExecute(Sender: TObject);
var
  ModifierList: TModifierList;
begin
  ModifierList := TModifierList.Create;
  try
    GetModifiers(ModifierList);

    if TModifierListEditorForm.OpenModal(ModifierList) then
      SetModifiers(ModifierList);
  finally
    ModifierList.Free;
  end;
end;

procedure TSectionTreeFrame.actMoveDownExecute(Sender: TObject);
begin
  MoveSection(TRUE);
end;

procedure TSectionTreeFrame.actMoveUpExecute(Sender: TObject);
begin
  MoveSection(FALSE);
end;

procedure TSectionTreeFrame.actPasteExecute(Sender: TObject);
begin
  Dispatcher.PerformAction(Id, 'paste');
end;

procedure TSectionTreeFrame.actPasteUpdate(Sender: TObject);
begin
  (Sender as TAction).Enabled := ClipboardHasFormatName(QUESTION_LIST_CLIPBOARD_FORMAT);
end;

procedure TSectionTreeFrame.RenderSections(ChangedOnly: Boolean);
var
  Node: TTreeNode;
begin
  Node := tvSections.Items.GetFirstNode;
  while Node <> nil do
  begin
    RenderSection(Node, ChangedOnly);
    Node := Node.GetNext;
  end;
end;

procedure TSectionTreeFrame.RenderSelection;
var
  Saved: TNotifyEvent;
begin
  Saved := tvSections.OnSelectionChanged;
  try
    tvSections.OnSelectionChanged := nil;
    tvSections.Selected := GetNode(FSelection.S['selectedEditor']);
  finally
    tvSections.OnSelectionChanged := Saved;
  end;
end;

procedure TSectionTreeFrame.CreateSectionNode(const Node: ISuperObject;
  Parent: TTreeNode);
var
  TreeNode: TTreeNode;
  Child: ISuperObject;
begin
  TreeNode := tvSections.Items.AddChild(Parent, '');
  TreeNode.Data := TSectionHolder.Create(Node.S['editor']);

  for Child in Node['child'] do
    CreateSectionNode(Child, TreeNode);
end;

function TSectionTreeFrame.SelectedEditorId: TShapeId;
begin
  Result := GetNodeEditorId(tvSections.Selected);
end;

function TSectionTreeFrame.SelectedEditor: ISuperObject;
begin
  Result := GetNodeEditor(tvSections.Selected);
end;

function TSectionTreeFrame.GetNodeEditorId(Node: TTreeNode): TShapeId;
begin
  Result := TSectionHolder(Node.Data).FEditorId;
end;

function TSectionTreeFrame.GetNodeEditor(Node: TTreeNode): ISuperObject;
begin
  Result := TSectionHolder(Node.Data).FEditor;
end;

procedure TSectionTreeFrame.UpdateActions;
var
  Root, Selected: TTreeNode;
begin
  Root := tvSections.Items.GetFirstNode;
  Selected := tvSections.Selected;

  actMoveUp.Enabled := (Selected <> Root)
    and ((Selected.Parent <> Root) or (Selected.Index > 0));

  actMoveDown.Enabled := (Selected <> Root)
    and ((Selected.Parent <> Root) or (Selected.Index < Root.Count-1));
end;

procedure TSectionTreeFrame.MoveSection(Down: Boolean);
var
  Input: ISuperObject;
begin
  Input := SO;
  Input.B['forward'] := Down;
  Dispatcher.PerformAction(Id, 'move', Input);
end;

procedure TSectionTreeFrame.GetModifiers(List: TModifierList);
var
  Res: ISuperObject;
begin
  Res := Dispatcher.PerformAction(SelectedEditorId, 'getModifiers');
  BytesToModifierList(DecodeStringBase64(Res.S['modifiers']), List);
end;

procedure TSectionTreeFrame.SetModifiers(List: TModifierList);
var
  Input: ISuperObject;
begin
  Input := SO;
  Input.S['modifiers'] := EncodeStringBase64(ModifierListToBytes(List));
  Dispatcher.PerformAction(SelectedEditorId, 'setModifiers', Input);
end;

end.

